﻿using System;
using System.Collections.Generic;

using Microsoft.Xna.Framework;

namespace AlfarEngine.Components
{
    /// <summary>
    /// Contain data of a Collision
    /// </summary>
    public struct CollisionData
    {
        public enum CollisionType { ENTER, STAY, EXIT }

        /// <summary>
        /// The type of collision (enter, stay, exit)
        /// </summary>
        public CollisionType type;
        /// <summary>
        /// the other collider
        /// </summary>
        public Collider collider;

        //Allow to know if this collider was already check this time (usefull we a collider is twice or more in the tree)
        internal bool frameChecked;
    }

    /// <summary>
    /// A gameobject derivate that implemente this interface will receveive OnCollisionEnter/Stay/exit message
    /// </summary>
    public interface ICollidable
    {
        void OnCollisionEnter(CollisionData pColliders);
        void OnCollisionStay(CollisionData pColliders);
        void OnCollisionExit(CollisionData pColliders);
    }

    /// <summary>
    /// Add a Collider to this GameObject. A Collider will detect collision with other collider in the scene.\n
    /// If the Game Object derivate that have this collider implement the ICollidable interface, it will receive three message :
    /// 
    /// OnCollisionEnter on the first contact \n
    /// OnCollisionStay on the following frame if still in contact \n
    /// OnCollisionExit when it leave the contact \n
    /// 
    /// If you wanna move the GameObject during one of this callback, use MoveTo of the collider, this will cause the engine to reset the flag of contact.\n
    /// It will allow to receive once ahain a *Enter callback if still in contact. Usefull when you use the callback to PREVENT to enter in the collider.\n
    /// It will generate a *Enter message each time you still enter the collider, allowing to "get out" each time without having to make a *Stay.\n
    /// </summary>
    public class Collider : AComponent
    {
        #region Variables

        protected Rectangle mRect;
        protected Rectangle mLocalRect;

        //--- strictly intern, allow to build a tree for fast collision checking
        static protected Collider sRoot = null;

        protected Collider mParent;
        protected List<Collider> mChildren;
        protected ICollidable mCollidable;

        protected Dictionary<Collider, CollisionData> mCollisionData;

        protected bool mDynamic = false;
        protected Color mColor;

        protected Vector2 mPreviousPos;
        protected bool mWasMoved = false;

        protected Collider mCurrentCollider;

        #endregion

        //-------------------------------------------

        #region Accessors

        /// <summary>
        /// Get the local bound
        /// </summary>
        public Rectangle bounds { get { return mLocalRect; } set { mLocalRect = value; Collider.InsertIntoTree(this); } }

        public Rectangle worldBounds { get { return mRect; } }
        /// <summary>
        /// set this collider as dynamic : if dynamic, the collider is not inserted into the tree
        /// </summary>
        public bool dynamic { get { return mDynamic; } set { mDynamic = value; } }

        public Vector2 previousPosition { get { return mPreviousPos; } }

        #endregion

        //----------------------------------------------

        #region Methods

        public Collider(GameObject pOwner)
        :base(pOwner)
        {
            if (sRoot == null && pOwner != null)
                sRoot = new Collider(null);

            mLocalRect = Rectangle.Empty;
            mRect = Rectangle.Empty;

            mParent = null;
            mChildren = new List<Collider>();

            mCollidable = mOwner as ICollidable;
            mCollisionData = new Dictionary<Collider, CollisionData>();

            mDynamic = false;

            Random lrand = new Random();
            mColor = new Color((float)lrand.NextDouble(), (float)lrand.NextDouble(), (float)lrand.NextDouble(), 1.0f);

            //mPreviousPos = mOwner.position;
        }

        public override void Draw(GameTime pTime, Color pColor)
        {
            //Utilities.Debug.DrawRectangle(mRect);
        }

        public override void Update(GameTime pTime)
        {
            mCurrentCollider = null;

            if (mOwner.dirty)
            {//owner dirty we need to reinsert his inside the collider tree
                Collider.InsertIntoTree(this);
            }

            if (mCollidable == null)
            {//no need to check collision, the owner doesn't catch them.
                return;
            } 

            CollisionChecking();

            mPreviousPos = mOwner.position;
        }

        /// <summary>
        /// Call this inside a OnCollision* Event.
        /// It return the position between current and previous position that is not int the collider
        /// This will "slide" along the collider
        /// </summary>
        /// <returns></returns>
        public Vector2 SlideAlong()
        {

            /*if (mCurrentCollider == null)
                return owner.position;*/

            Vector2 lTemp = mPreviousPos;

            Rectangle lHoriRect = new Rectangle((int)mPreviousPos.X - (int)(mRect.Width * 0.5f), (int)owner.position.Y - (int)(mRect.Height * 0.5f), mRect.Width, mRect.Height);
            Rectangle lVertiRect = new Rectangle((int)owner.position.X - (int)(mRect.Width * 0.5f), (int)mPreviousPos.Y - (int)(mRect.Height * 0.5f), mRect.Width, mRect.Height);

            if (!lHoriRect.Intersects(mCurrentCollider.mRect))
            {
                lTemp.Y = owner.position.Y;
            }

            if (!lVertiRect.Intersects(mCurrentCollider.mRect))
            {
                lTemp.X = owner.position.X;
            }

            return lTemp;
        }

        public void MoveTo(Vector2 pPos)
        {
            mOwner.position = pPos;
            mWasMoved = true;
        }

        public override void Serialize(System.IO.BinaryWriter pFile)
        {
            base.Serialize(pFile);

            pFile.Write(mLocalRect.X);
            pFile.Write(mLocalRect.Y);
            pFile.Write(mLocalRect.Width);
            pFile.Write(mLocalRect.Height);


        }

        public override void Deserialize(System.IO.BinaryReader pFile)
        {
            base.Deserialize(pFile);

            bounds = new Rectangle(pFile.ReadInt32(), pFile.ReadInt32(), pFile.ReadInt32(), pFile.ReadInt32());
        }

        //********************* PRIVATE****************

        static private void InsertIntoTree(Collider pCollider)
        {
            pCollider.mRect = new Rectangle((int)pCollider.mOwner.position.X - (int)(pCollider.mLocalRect.Width * 0.5f),
                                            (int)pCollider.mOwner.position.Y - (int)(pCollider.mLocalRect.Height * 0.5f),
                                            pCollider.mLocalRect.Width,
                                            pCollider.mLocalRect.Height);

            if (pCollider.mDynamic)
                return;

            if (pCollider.mParent != null && pCollider.mRect.Intersects(pCollider.mParent.mRect))
            {//in case it still is inside the parent rect we don't need to change its place
                return;
            }

            if(pCollider.mParent != null)
                pCollider.mParent.mChildren.Remove(pCollider);

            sRoot.RecersiveIntersectInsert(pCollider);
        }

        private bool RecersiveIntersectInsert(Collider pCollider)
        {
            if (mOwner != null)
            {   //We cannot add this to a collider attached to a gameObject
                return false;//We return false so it's Add to the parent collider (that should be a "gameobject-less" collider")
            }

            if (mParent == null || pCollider.mRect.Intersects(mRect))
            {
                bool lIntersect = false;

                for (int i = 0; i < mChildren.Count; i++)
                {
                    lIntersect = lIntersect || mChildren[i].RecersiveIntersectInsert(pCollider);
                }

                if (!lIntersect) // intersect with none of lower collider, we add the collider as son of this
                {
                    mChildren.Add(pCollider);

                    mRect = Rectangle.Empty;
                    Rectangle lBiggest = Rectangle.Empty;
                    for (int i = 0; i < mChildren.Count; i++)
                    {
                        Rectangle.Union(ref mRect, ref mChildren[i].mRect, out mRect);
                        if (mChildren[i].mRect.Width * mChildren[i].mRect.Height >  lBiggest.Width * lBiggest.Height)
                        {
                            lBiggest = mChildren[i].mRect;
                        }
                    }

                    // ----- we check if the current area of this layer is not thrice as big as the biggest of it's "children"

                    List<Collider> lToAdd = new List<Collider>();

                    if (lBiggest.Width*3 < mRect.Width)
                    {//we split alongst width
                        Rectangle lLeft     = new Rectangle(mRect.X, mRect.Y, (int)(mRect.Width * 0.5f), mRect.Height);
                        Rectangle lRight    = new Rectangle(mRect.X + (int)(mRect.Width * 0.5f), mRect.Y, (int)(mRect.Width * 0.5f), mRect.Height);

                        Collider lLeftCollider = new Collider(null);
                        lLeftCollider.mRect = lLeft;
                        Collider lRightCollider = new Collider(null);
                        lRightCollider.mRect = lRight;

                        for (int i = 0; i < mChildren.Count; i++)
                        {
                            if (mChildren[i].mRect.Intersects(lLeft))
                            {
                                lLeftCollider.mChildren.Add(mChildren[i]);
                            }

                            if (mChildren[i].mRect.Intersects(lRight))
                            {
                                lRightCollider.mChildren.Add(mChildren[i]);
                            }
                        }

                        lToAdd.Add(lLeftCollider);
                        lToAdd.Add(lRightCollider);
                    }
                    else if (lBiggest.Height * 3 < mRect.Height)
                    {//we split alongst height
                        Rectangle lLeft = new Rectangle(mRect.X, mRect.Y, mRect.Width, (int)(mRect.Height * 0.5f));
                        Rectangle lRight = new Rectangle(mRect.X, mRect.Y + (int)(mRect.Height * 0.5f), mRect.Width , (int)(mRect.Height * 0.5f));

                        Collider lLeftCollider = new Collider(null);
                        lLeftCollider.mRect = lLeft;
                        Collider lRightCollider = new Collider(null);
                        lRightCollider.mRect = lRight;

                        for (int i = 0; i < mChildren.Count; i++)
                        {
                            if (mChildren[i].mRect.Intersects(lLeft))
                            {
                                lLeftCollider.mChildren.Add(mChildren[i]);
                            }

                            if (mChildren[i].mRect.Intersects(lRight))
                            {
                                lRightCollider.mChildren.Add(mChildren[i]);
                            }
                        }

                        lToAdd.Add(lLeftCollider);
                        lToAdd.Add(lRightCollider);
                    }

                    if (lToAdd.Count != 0)
                    {
                        mChildren.Clear();
                        mChildren.AddRange(lToAdd);
                    }

                    return true;
                }

                return lIntersect;
            }

            return false;
        }

        //--

        private void CollideWithScene(Collider pCollider)
        {
            if (!pCollider.mRect.Intersects(mRect))
                return;

            if (mOwner != null && this != pCollider)
            {//it's a leaf !

                if (pCollider.mCollisionData.ContainsKey(this) && !pCollider.mCollisionData[this].frameChecked)
                {//the collider is already in the collection, we just put its state to "stay"
                    CollisionData lData = pCollider.mCollisionData[this];
                    lData.type = CollisionData.CollisionType.STAY;
                    pCollider.mCollisionData[this] = lData;
                }
                else
                {//we add it if not already in, we the state "enter"
                    CollisionData lData = new CollisionData();
                    lData.collider = this;
                    lData.type = CollisionData.CollisionType.ENTER;
                    lData.frameChecked = true;
                    pCollider.mCollisionData[this] = lData;
                }
            }

            for (int i = 0; i < mChildren.Count; i++)
            {
                mChildren[i].CollideWithScene(pCollider);
            }

            return;
        }

        private void CollisionChecking()
        {
            sRoot.CollideWithScene(this);

            List<Collider> lToPutOnExit = new List<Collider>();
            List<Collider> lToRemove = new List<Collider>();

            foreach (Collider lCollider in mCollisionData.Keys)
            {
                CollisionData lData = mCollisionData[lCollider];
                switch (mCollisionData[lCollider].type)
                {
                    case CollisionData.CollisionType.ENTER:
                        mCurrentCollider = lData.collider;
                        mCollidable.OnCollisionEnter(lData);

                        if (mWasMoved)
                        {
                            lToRemove.Add(lCollider);
                            mWasMoved = false;
                        }
                        else
                        {
                            lToPutOnExit.Add(lCollider);
                        }
                        break;
                    case CollisionData.CollisionType.STAY:
                        mCurrentCollider = lData.collider;
                        mCollidable.OnCollisionStay(lData);
                        lToPutOnExit.Add(lCollider);
                        // we put the stay in exit stat. If still in stay, the next loop while put them on stay again.
                        break;
                    case CollisionData.CollisionType.EXIT:
                        mCurrentCollider = lData.collider;
                        mCollidable.OnCollisionExit(lData);
                        lToRemove.Add(lCollider);
                        break;
                    default: break;
                }
            }

            for (int i = 0; i < lToPutOnExit.Count; i++)
            {
                CollisionData lData = mCollisionData[lToPutOnExit[i]];
                lData.type = CollisionData.CollisionType.EXIT;
                lData.frameChecked = false;
                mCollisionData[lToPutOnExit[i]] = lData;
            }

            for (int i = 0; i < lToRemove.Count; i++)
            {
                mCollisionData.Remove(lToRemove[i]);
            }
        }




        static public void DrawGrid()
        {
            sRoot.drawGridRecurs();
        }

        private void drawGridRecurs()
        {
            for (int i = 0; i < mChildren.Count; i++)
            {
                mChildren[i].drawGridRecurs();
            }

            Utilities.Debug.DrawRectangle(mRect, mColor);
        }

        #endregion
    }
}
